/*
 * Copyright (c) 2008-2010, Matthias Mann
 *
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *     * Redistributions of source code must retain the above copyright notice,
 *       this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above copyright
 *       notice, this list of conditions and the following disclaimer in the
 *       documentation and/or other materials provided with the distribution.
 *     * Neither the name of Matthias Mann nor the names of its contributors may
 *       be used to endorse or promote products derived from this software
 *       without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package org.newdawn.slick.opengl;

import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.zip.CRC32;
import java.util.zip.DataFormatException;
import java.util.zip.Inflater;

/**
 * A PNGDecoder. The slick PNG decoder is based on this class :)
 * 
 * @author Matthias Mann
 */
public class PNGDecoder {
    public static Format ALPHA = new Format(1, true);
    public static Format LUMINANCE = new Format(1, false);
    public static Format LUMINANCE_ALPHA = new Format(2, true);
    public static Format RGB = new Format(3, false);
    public static Format RGBA = new Format(4, true);
    public static Format BGRA = new Format(4, true);
    public static Format ABGR = new Format(4, true);

    public static class Format {

        final int numComponents;
        final boolean hasAlpha;

        private Format(int numComponents, boolean hasAlpha) {
            this.numComponents = numComponents;
            this.hasAlpha = hasAlpha;
        }

        public int getNumComponents() {
            return numComponents;
        }

        public boolean isHasAlpha() {
            return hasAlpha;
        }
    }

    private static final byte[] SIGNATURE = {(byte)137, 80, 78, 71, 13, 10, 26, 10};

    private static final int IHDR = 0x49484452;
    private static final int PLTE = 0x504C5445;
    private static final int tRNS = 0x74524E53;
    private static final int IDAT = 0x49444154;
    private static final int IEND = 0x49454E44;
    
    private static final byte COLOR_GREYSCALE = 0;
    private static final byte COLOR_TRUECOLOR = 2;
    private static final byte COLOR_INDEXED = 3;
    private static final byte COLOR_GREYALPHA = 4;
    private static final byte COLOR_TRUEALPHA = 6;  
    
    private final InputStream input;
    private final CRC32 crc;
    private final byte[] buffer;
    
    private int chunkLength;
    private int chunkType;
    private int chunkRemaining;
    
    private int width;
    private int height;
    private int bitdepth;
    private int colorType;
    private int bytesPerPixel;
    private byte[] palette;
    private byte[] paletteA;
    private byte[] transPixel;
    
    public PNGDecoder(InputStream input) throws IOException {
        this.input = input;
        this.crc = new CRC32();
        this.buffer = new byte[4096];
        
        readFully(buffer, 0, SIGNATURE.length);
        if(!checkSignature(buffer)) {
            throw new IOException("Not a valid PNG file");
        }
        
        openChunk(IHDR);
        readIHDR();
        closeChunk();
        
        searchIDAT: for(;;) {
            openChunk();
            switch (chunkType) {
            case IDAT:
                break searchIDAT;
            case PLTE:
                readPLTE();
                break;
            case tRNS:
                readtRNS();
                break;
            }
            closeChunk();
        }

        if(colorType == COLOR_INDEXED && palette == null) {
            throw new IOException("Missing PLTE chunk");
        }
    }

    public int getHeight() {
        return height;
    }

    public int getWidth() {
        return width;
    }
    
    public boolean hasAlpha() {
        return colorType == COLOR_TRUEALPHA ||
                paletteA != null || transPixel != null;
    }
    
    public boolean isRGB() {
        return colorType == COLOR_TRUEALPHA ||
                colorType == COLOR_TRUECOLOR ||
                colorType == COLOR_INDEXED;
    }

    /**
     * Computes the implemented format conversion for the desired format.
     *
     * @param fmt the desired format
     * @return format which best matches the desired format
     * @throws UnsupportedOperationException if this PNG file can't be decoded
     */
    public Format decideTextureFormat(Format fmt) {
        switch (colorType) {
        case COLOR_TRUECOLOR:
        	if ((fmt == ABGR) || (fmt == RGBA) || (fmt == BGRA) || (fmt == RGB)) {
        		return fmt;
        	}
            
        	return RGB;
        case COLOR_TRUEALPHA:
        	if ((fmt == ABGR) || (fmt == RGBA) || (fmt == BGRA) || (fmt == RGB)) {
        		return fmt;
        	}
            
        	return RGBA;
        case COLOR_GREYSCALE:
        	if ((fmt == LUMINANCE) || (fmt == ALPHA)) {
        		return fmt;
        	}
            
        	return LUMINANCE;
        case COLOR_GREYALPHA:
            return LUMINANCE_ALPHA;
        case COLOR_INDEXED:
        	if ((fmt == ABGR) || (fmt == RGBA) || (fmt == BGRA)) {
        		return fmt;
        	}
        	
        	return RGBA;
        default:
            throw new UnsupportedOperationException("Not yet implemented");
        }
    }
    
    public void decode(ByteBuffer buffer, int stride, Format fmt) throws IOException {
        final int offset = buffer.position();
        final int lineSize = ((width * bitdepth + 7) / 8) * bytesPerPixel;
        byte[] curLine = new byte[lineSize+1];
        byte[] prevLine = new byte[lineSize+1];
        byte[] palLine = (bitdepth < 8) ? new byte[width+1] : null;
        
        final Inflater inflater = new Inflater();
        try {
            for(int y=0 ; y<height ; y++) {
                readChunkUnzip(inflater, curLine, 0, curLine.length);
                unfilter(curLine, prevLine);

                buffer.position(offset + y*stride);

                switch (colorType) {
                case COLOR_TRUECOLOR:
                    if (fmt == ABGR) {
                    	copyRGBtoABGR(buffer, curLine); 
                    }
                    else if (fmt == RGBA) {
                    	copyRGBtoRGBA(buffer, curLine); 
                    }
                    else if (fmt == BGRA) {
                    	copyRGBtoBGRA(buffer, curLine); 
                    }
                    else if (fmt == RGB) {
                    	copy(buffer, curLine); 
                    } else {
                    	throw new UnsupportedOperationException("Unsupported format for this image");
                    }
                    break;
                case COLOR_TRUEALPHA:
                    if (fmt == ABGR) {
                    	copyRGBAtoABGR(buffer, curLine);
                    } else if (fmt == RGBA) { 
                    	copy(buffer, curLine);
                    } else if (fmt == BGRA) {
                    	copyRGBAtoBGRA(buffer, curLine); break;
                    } else if (fmt == RGB) {
                    	copyRGBAtoRGB(buffer, curLine); break;
                    } else {
                        throw new UnsupportedOperationException("Unsupported format for this image");
                    }
                    break;
                case COLOR_GREYSCALE:
                    if ((fmt == LUMINANCE) || (fmt == ALPHA)) {
                    	copy(buffer, curLine);
                    } else {
                    	throw new UnsupportedOperationException("Unsupported format for this image");
                    }
                    break;
                case COLOR_GREYALPHA:
                    if (fmt == LUMINANCE_ALPHA) {
                    	copy(buffer, curLine);
                    } else {
                    	throw new UnsupportedOperationException("Unsupported format for this image");
                    }
                    break;
                case COLOR_INDEXED:
                    switch(bitdepth) {
                        case 8: palLine = curLine; break;
                        case 4: expand4(curLine, palLine); break;
                        case 2: expand2(curLine, palLine); break;
                        case 1: expand1(curLine, palLine); break;
                        default: throw new UnsupportedOperationException("Unsupported bitdepth for this image");
                    }
                    if (fmt == ABGR) {
                    	copyPALtoABGR(buffer, palLine);
                    } else if (fmt == RGBA) {
                    	copyPALtoRGBA(buffer, palLine);
                    } else if (fmt == BGRA) {
                    	copyPALtoBGRA(buffer, palLine);
                    } else {
                    	throw new UnsupportedOperationException("Unsupported format for this image");
                    }
                    break;
                default:
                    throw new UnsupportedOperationException("Not yet implemented");
                }

                byte[] tmp = curLine;
                curLine = prevLine;
                prevLine = tmp;
            }
        } finally {
            inflater.end();
        }
    }
    
    private void copy(ByteBuffer buffer, byte[] curLine) {
        buffer.put(curLine, 1, curLine.length-1);
    }

    private void copyRGBtoABGR(ByteBuffer buffer, byte[] curLine) {
        if(transPixel != null) {
            byte tr = transPixel[1];
            byte tg = transPixel[3];
            byte tb = transPixel[5];
            for(int i=1,n=curLine.length ; i<n ; i+=3) {
                byte r = curLine[i];
                byte g = curLine[i+1];
                byte b = curLine[i+2];
                byte a = (byte)0xFF;
                if(r==tr && g==tg && b==tb) {
                    a = 0;
                }
                buffer.put(a).put(b).put(g).put(r);
            }
        } else {
            for(int i=1,n=curLine.length ; i<n ; i+=3) {
                buffer.put((byte)0xFF).put(curLine[i+2]).put(curLine[i+1]).put(curLine[i]);
            }
        }
    }

    private void copyRGBtoRGBA(ByteBuffer buffer, byte[] curLine) {
        if(transPixel != null) {
            byte tr = transPixel[1];
            byte tg = transPixel[3];
            byte tb = transPixel[5];
            for(int i=1,n=curLine.length ; i<n ; i+=3) {
                byte r = curLine[i];
                byte g = curLine[i+1];
                byte b = curLine[i+2];
                byte a = (byte)0xFF;
                if(r==tr && g==tg && b==tb) {
                    a = 0;
                }
                buffer.put(r).put(g).put(b).put(a);
            }
        } else {
            for(int i=1,n=curLine.length ; i<n ; i+=3) {
                buffer.put(curLine[i]).put(curLine[i+1]).put(curLine[i+2]).put((byte)0xFF);
            }
        }
    }

    private void copyRGBtoBGRA(ByteBuffer buffer, byte[] curLine) {
        if(transPixel != null) {
            byte tr = transPixel[1];
            byte tg = transPixel[3];
            byte tb = transPixel[5];
            for(int i=1,n=curLine.length ; i<n ; i+=3) {
                byte r = curLine[i];
                byte g = curLine[i+1];
                byte b = curLine[i+2];
                byte a = (byte)0xFF;
                if(r==tr && g==tg && b==tb) {
                    a = 0;
                }
                buffer.put(b).put(g).put(r).put(a);
            }
        } else {
            for(int i=1,n=curLine.length ; i<n ; i+=3) {
                buffer.put(curLine[i+2]).put(curLine[i+1]).put(curLine[i]).put((byte)0xFF);
            }
        }
    }

    private void copyRGBAtoABGR(ByteBuffer buffer, byte[] curLine) {
        for(int i=1,n=curLine.length ; i<n ; i+=4) {
            buffer.put(curLine[i+3]).put(curLine[i+2]).put(curLine[i+1]).put(curLine[i]);
        }
    }

    private void copyRGBAtoBGRA(ByteBuffer buffer, byte[] curLine) {
        for(int i=1,n=curLine.length ; i<n ; i+=4) {
            buffer.put(curLine[i+2]).put(curLine[i+1]).put(curLine[i+0]).put(curLine[i+3]);
        }
    }

    private void copyRGBAtoRGB(ByteBuffer buffer, byte[] curLine) {
        for(int i=1,n=curLine.length ; i<n ; i+=4) {
            buffer.put(curLine[i]).put(curLine[i+1]).put(curLine[i+2]);
        }
    }

    private void copyPALtoABGR(ByteBuffer buffer, byte[] curLine) {
        if(paletteA != null) {
            for(int i=1,n=curLine.length ; i<n ; i+=1) {
                int idx = curLine[i] & 255;
                byte r = palette[idx*3 + 0];
                byte g = palette[idx*3 + 1];
                byte b = palette[idx*3 + 2];
                byte a = paletteA[idx];
                buffer.put(a).put(b).put(g).put(r);
            }
        } else {
            for(int i=1,n=curLine.length ; i<n ; i+=1) {
                int idx = curLine[i] & 255;
                byte r = palette[idx*3 + 0];
                byte g = palette[idx*3 + 1];
                byte b = palette[idx*3 + 2];
                byte a = (byte)0xFF;
                buffer.put(a).put(b).put(g).put(r);
            }
        }
    }

    private void copyPALtoRGBA(ByteBuffer buffer, byte[] curLine) {
        if(paletteA != null) {
            for(int i=1,n=curLine.length ; i<n ; i+=1) {
                int idx = curLine[i] & 255;
                byte r = palette[idx*3 + 0];
                byte g = palette[idx*3 + 1];
                byte b = palette[idx*3 + 2];
                byte a = paletteA[idx];
                buffer.put(r).put(g).put(b).put(a);
            }
        } else {
            for(int i=1,n=curLine.length ; i<n ; i+=1) {
                int idx = curLine[i] & 255;
                byte r = palette[idx*3 + 0];
                byte g = palette[idx*3 + 1];
                byte b = palette[idx*3 + 2];
                byte a = (byte)0xFF;
                buffer.put(r).put(g).put(b).put(a);
            }
        }
    }

    private void copyPALtoBGRA(ByteBuffer buffer, byte[] curLine) {
        if(paletteA != null) {
            for(int i=1,n=curLine.length ; i<n ; i+=1) {
                int idx = curLine[i] & 255;
                byte r = palette[idx*3 + 0];
                byte g = palette[idx*3 + 1];
                byte b = palette[idx*3 + 2];
                byte a = paletteA[idx];
                buffer.put(b).put(g).put(r).put(a);
            }
        } else {
            for(int i=1,n=curLine.length ; i<n ; i+=1) {
                int idx = curLine[i] & 255;
                byte r = palette[idx*3 + 0];
                byte g = palette[idx*3 + 1];
                byte b = palette[idx*3 + 2];
                byte a = (byte)0xFF;
                buffer.put(b).put(g).put(r).put(a);
            }
        }
    }

    private void expand4(byte[] src, byte[] dst) {
        for(int i=1,n=dst.length ; i<n ; i+=2) {
            int val = src[1 + (i >> 1)] & 255;
            switch(n-i) {
                default: dst[i+1] = (byte)(val & 15);
                case 1:  dst[i  ] = (byte)(val >> 4);
            }
        }
    }

    private void expand2(byte[] src, byte[] dst) {
        for(int i=1,n=dst.length ; i<n ; i+=4) {
            int val = src[1 + (i >> 2)] & 255;
            switch(n-i) {
                default: dst[i+3] = (byte)((val     ) & 3);
                case 3:  dst[i+2] = (byte)((val >> 2) & 3);
                case 2:  dst[i+1] = (byte)((val >> 4) & 3);
                case 1:  dst[i  ] = (byte)((val >> 6)    );
            }
        }
    }

    private void expand1(byte[] src, byte[] dst) {
        for(int i=1,n=dst.length ; i<n ; i+=8) {
            int val = src[1 + (i >> 3)] & 255;
            switch(n-i) {
                default: dst[i+7] = (byte)((val     ) & 1);
                case 7:  dst[i+6] = (byte)((val >> 1) & 1);
                case 6:  dst[i+5] = (byte)((val >> 2) & 1);
                case 5:  dst[i+4] = (byte)((val >> 3) & 1);
                case 4:  dst[i+3] = (byte)((val >> 4) & 1);
                case 3:  dst[i+2] = (byte)((val >> 5) & 1);
                case 2:  dst[i+1] = (byte)((val >> 6) & 1);
                case 1:  dst[i  ] = (byte)((val >> 7)    );
            }
        }
    }
    
    private void unfilter(byte[] curLine, byte[] prevLine) throws IOException {
        switch (curLine[0]) {
            case 0: // none
                break;
            case 1:
                unfilterSub(curLine);
                break;
            case 2:
                unfilterUp(curLine, prevLine);
                break;
            case 3:
                unfilterAverage(curLine, prevLine);
                break;
            case 4:
                unfilterPaeth(curLine, prevLine);
                break;
            default:
                throw new IOException("invalide filter type in scanline: " + curLine[0]);
        }
    }
    
    private void unfilterSub(byte[] curLine) {
        final int bpp = this.bytesPerPixel;
        for(int i=bpp+1,n=curLine.length ; i<n ; ++i) {
            curLine[i] += curLine[i-bpp];
        }
    }
    
    private void unfilterUp(byte[] curLine, byte[] prevLine) {
        final int bpp = this.bytesPerPixel;
        for(int i=1,n=curLine.length ; i<n ; ++i) {
            curLine[i] += prevLine[i];
        }
    }
    
    private void unfilterAverage(byte[] curLine, byte[] prevLine) {
        final int bpp = this.bytesPerPixel;
        
        int i;
        for(i=1 ; i<=bpp ; ++i) {
            curLine[i] += (byte)((prevLine[i] & 0xFF) >>> 1);
        }
        for(int n=curLine.length ; i<n ; ++i) {
            curLine[i] += (byte)(((prevLine[i] & 0xFF) + (curLine[i - bpp] & 0xFF)) >>> 1);
        }
    }
    
    private void unfilterPaeth(byte[] curLine, byte[] prevLine) {
        final int bpp = this.bytesPerPixel;
        
        int i;
        for(i=1 ; i<=bpp ; ++i) {
            curLine[i] += prevLine[i];
        }
        for(int n=curLine.length ; i<n ; ++i) {
            int a = curLine[i - bpp] & 255;
            int b = prevLine[i] & 255;
            int c = prevLine[i - bpp] & 255;
            int p = a + b - c;
            int pa = p - a; if(pa < 0) pa = -pa;
            int pb = p - b; if(pb < 0) pb = -pb;
            int pc = p - c; if(pc < 0) pc = -pc;
            if(pa<=pb && pa<=pc)
                c = a;
            else if(pb<=pc)
                c = b;
            curLine[i] += (byte)c;
        }
    }
      
    private void readIHDR() throws IOException {
        checkChunkLength(13);
        readChunk(buffer, 0, 13);
        width = readInt(buffer, 0);
        height = readInt(buffer, 4);
        bitdepth = buffer[8] & 255;
        colorType = buffer[9] & 255;
        
        switch (colorType) {
        case COLOR_GREYSCALE:
            if(bitdepth != 8) {
                throw new IOException("Unsupported bit depth: " + bitdepth);
            }
            bytesPerPixel = 1;
            break;
        case COLOR_GREYALPHA:
            if(bitdepth != 8) {
                throw new IOException("Unsupported bit depth: " + bitdepth);
            }
            bytesPerPixel = 2;
            break;
        case COLOR_TRUECOLOR:
            if(bitdepth != 8) {
                throw new IOException("Unsupported bit depth: " + bitdepth);
            }
            bytesPerPixel = 3;
            break;
        case COLOR_TRUEALPHA:
            if(bitdepth != 8) {
                throw new IOException("Unsupported bit depth: " + bitdepth);
            }
            bytesPerPixel = 4;
            break;
        case COLOR_INDEXED:
            switch(bitdepth) {
            case 8:
            case 4:
            case 2:
            case 1:
                bytesPerPixel = 1;
                break;
            default:
                throw new IOException("Unsupported bit depth: " + bitdepth);
            }
            break;
        default:
            throw new IOException("unsupported color format: " + colorType);
        }
        
        if(buffer[10] != 0) {
            throw new IOException("unsupported compression method");
        }
        if(buffer[11] != 0) {
            throw new IOException("unsupported filtering method");
        }
        if(buffer[12] != 0) {
            throw new IOException("unsupported interlace method");
        }
    }

    private void readPLTE() throws IOException {
        int paletteEntries = chunkLength / 3;
        if(paletteEntries < 1 || paletteEntries > 256 || (chunkLength % 3) != 0) {
            throw new IOException("PLTE chunk has wrong length");
        }
        palette = new byte[paletteEntries*3];
        readChunk(palette, 0, palette.length);
    }

    private void readtRNS() throws IOException {
        switch (colorType) {
        case COLOR_GREYSCALE:
            checkChunkLength(2);
            transPixel = new byte[2];
            readChunk(transPixel, 0, 2);
            break;
        case COLOR_TRUECOLOR:
            checkChunkLength(6);
            transPixel = new byte[6];
            readChunk(transPixel, 0, 6);
            break;
        case COLOR_INDEXED:
            if(palette == null) {
                throw new IOException("tRNS chunk without PLTE chunk");
            }
            paletteA = new byte[palette.length/3];
            Arrays.fill(paletteA, (byte)0xFF);
            readChunk(paletteA, 0, paletteA.length);
            break;
        default:
            // just ignore it
        }
    }
    
    private void closeChunk() throws IOException {
        if(chunkRemaining > 0) {
            // just skip the rest and the CRC
            skip(chunkRemaining + 4);
        } else {
            readFully(buffer, 0, 4);
            int expectedCrc = readInt(buffer, 0);
            int computedCrc = (int)crc.getValue();
            if(computedCrc != expectedCrc) {
                throw new IOException("Invalid CRC");
            }
        }
        chunkRemaining = 0;
        chunkLength = 0;
        chunkType = 0;
    }
    
    private void openChunk() throws IOException {
        readFully(buffer, 0, 8);
        chunkLength = readInt(buffer, 0);
        chunkType = readInt(buffer, 4);
        chunkRemaining = chunkLength;
        crc.reset();
        crc.update(buffer, 4, 4);   // only chunkType
    }
    
    private void openChunk(int expected) throws IOException {
        openChunk();
        if(chunkType != expected) {
            throw new IOException("Expected chunk: " + Integer.toHexString(expected));
        }
    }

    private void checkChunkLength(int expected) throws IOException {
        if(chunkLength != expected) {
            throw new IOException("Chunk has wrong size");
        }
    }
    
    private int readChunk(byte[] buffer, int offset, int length) throws IOException {
        if(length > chunkRemaining) {
            length = chunkRemaining;
        }
        readFully(buffer, offset, length);
        crc.update(buffer, offset, length);
        chunkRemaining -= length;
        return length;
    }

    private void refillInflater(Inflater inflater) throws IOException {
        while(chunkRemaining == 0) {
            closeChunk();
            openChunk(IDAT);
        }
        int read = readChunk(buffer, 0, buffer.length);
        inflater.setInput(buffer, 0, read);
    }
    
    private void readChunkUnzip(Inflater inflater, byte[] buffer, int offset, int length) throws IOException {
        try {
            do {
                int read = inflater.inflate(buffer, offset, length);
                if(read <= 0) {
                    if(inflater.finished()) {
                        throw new EOFException();
                    }
                    if(inflater.needsInput()) {
                        refillInflater(inflater);
                    } else {
                        throw new IOException("Can't inflate " + length + " bytes");
                    }
                } else {
                    offset += read;
                    length -= read;
                }
            } while(length > 0);
        } catch (DataFormatException ex) {
            throw (IOException)(new IOException("inflate error").initCause(ex));
        }
    }

    private void readFully(byte[] buffer, int offset, int length) throws IOException {
        do {
            int read = input.read(buffer, offset, length);
            if(read < 0) {
                throw new EOFException();
            }
            offset += read;
            length -= read;
        } while(length > 0);
    }
    
    private int readInt(byte[] buffer, int offset) {
        return
                ((buffer[offset  ]      ) << 24) |
                ((buffer[offset+1] & 255) << 16) |
                ((buffer[offset+2] & 255) <<  8) |
                ((buffer[offset+3] & 255)      );
    }

    private void skip(long amount) throws IOException {
        while(amount > 0) {
            long skipped = input.skip(amount);
            if(skipped < 0) {
                throw new EOFException();
            }
            amount -= skipped;
        }
    }
    
    private static boolean checkSignature(byte[] buffer) {
        for(int i=0 ; i<SIGNATURE.length ; i++) {
            if(buffer[i] != SIGNATURE[i]) {
                return false;
            }
        }
        return true;
    }
}
